Skip to content

Conversation

@pcarleton
Copy link
Member

Summary

Adds an auth-test-server to the conformance test suite for testing server-side OAuth implementation.

What's New

  • src/conformance/auth-test-server.ts - MCP server with OAuth authentication
  • Updated src/conformance/README.md with documentation

Features

  • Uses SDK's requireBearerAuth middleware for authentication
  • Validates tokens via the authorization server's introspection endpoint (RFC 7662)
  • Serves Protected Resource Metadata at /.well-known/oauth-protected-resource
  • Requires MCP_CONFORMANCE_AUTH_SERVER_URL environment variable

Usage

# Start with a fake auth server
MCP_CONFORMANCE_AUTH_SERVER_URL=http://localhost:3000 \
  npx tsx src/conformance/auth-test-server.ts

Related

This server is used by the conformance repo's server auth tests (modelcontextprotocol/conformance#105).

- MCP server with Bearer token authentication
- Uses SDK's requireBearerAuth middleware
- Validates tokens via AS introspection endpoint (RFC 7662)
- Serves Protected Resource Metadata at /.well-known/oauth-protected-resource
- Designed for server auth conformance tests
@pcarleton pcarleton requested a review from a team as a code owner January 14, 2026 12:02
@changeset-bot
Copy link

changeset-bot bot commented Jan 14, 2026

⚠️ No Changeset found

Latest commit: 23b61b6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 14, 2026

Open in StackBlitz

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@1384
npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@1384

commit: 23b61b6

});

// Handle GET requests - SSE streams for sessions (also requires auth)
app.get('/mcp', bearerAuth, async (req: Request, res: Response) => {

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI about 23 hours ago

In general, to fix missing rate limiting on HTTP handlers that perform authorization or expensive operations, add a rate-limiting middleware (ideally using a well-known library) and apply it to the sensitive routes. The middleware should limit the number of requests per client (e.g., per IP) over a time window, returning an error (often 429) when the limit is exceeded. This reduces the risk of denial-of-service by preventing individual clients from overwhelming the server.

For this file, the least invasive and most standard fix is to use the express-rate-limit package. We will:

  1. Import express-rate-limit near the other imports.
  2. Define a mcpRateLimiter instance with reasonable defaults (e.g., number of requests per 15 minutes) and a message appropriate for this auth test server.
  3. Apply mcpRateLimiter specifically to the /mcp routes (POST, GET, DELETE) by inserting it in the middleware chain immediately after bearerAuth (so unauthorized requests are still cheap while authenticated requests are rate-limited). This addresses all three alert variants because they are all on the same route family (/mcp) with the same missing rate limiting concern.

We only touch src/conformance/auth-test-server.ts. The changes are:

  • Add import rateLimit from 'express-rate-limit'; along with existing imports.
  • Inside startServer, after setting up const app = express(); (in the part of the file not fully shown but present), define const mcpRateLimiter = rateLimit({ ... }).
  • Update three route definitions:
    • app.post('/mcp', bearerAuth, adminScopeCheck, async ...) becomes app.post('/mcp', bearerAuth, mcpRateLimiter, adminScopeCheck, async ...).
    • app.get('/mcp', bearerAuth, async ...) becomes app.get('/mcp', bearerAuth, mcpRateLimiter, async ...).
    • app.delete('/mcp', bearerAuth, async ...) becomes app.delete('/mcp', bearerAuth, mcpRateLimiter, async ...).

This preserves existing behavior (auth, scope checks, handler logic) while adding rate limiting to the authenticated routes only.


Suggested changeset 2
src/conformance/auth-test-server.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/conformance/auth-test-server.ts b/src/conformance/auth-test-server.ts
--- a/src/conformance/auth-test-server.ts
+++ b/src/conformance/auth-test-server.ts
@@ -24,6 +24,7 @@
 import express, { Request, Response, NextFunction } from 'express';
 import cors from 'cors';
 import { randomUUID } from 'crypto';
+import rateLimit from 'express-rate-limit';
 
 // Extend Express Request type to include auth info from SDK middleware
 declare module 'express' {
@@ -305,7 +306,15 @@
   );
 
   // Handle POST requests to /mcp with bearer auth and scope checking
-  app.post('/mcp', bearerAuth, adminScopeCheck, async (req: Request, res: Response) => {
+  const mcpRateLimiter = rateLimit({
+    windowMs: 15 * 60 * 1000, // 15 minutes
+    max: 100, // Limit each client to 100 requests per windowMs
+    standardHeaders: true,
+    legacyHeaders: false,
+    message: 'Too many requests to the MCP endpoint, please try again later.'
+  });
+
+  app.post('/mcp', bearerAuth, mcpRateLimiter, adminScopeCheck, async (req: Request, res: Response) => {
     const sessionId = req.headers['mcp-session-id'] as string | undefined;
 
     try {
@@ -371,7 +380,7 @@
   });
 
   // Handle GET requests - SSE streams for sessions (also requires auth)
-  app.get('/mcp', bearerAuth, async (req: Request, res: Response) => {
+  app.get('/mcp', bearerAuth, mcpRateLimiter, async (req: Request, res: Response) => {
     const sessionId = req.headers['mcp-session-id'] as string | undefined;
 
     if (!sessionId || !transports[sessionId]) {
@@ -393,7 +402,7 @@
   });
 
   // Handle DELETE requests - session termination (also requires auth)
-  app.delete('/mcp', bearerAuth, async (req: Request, res: Response) => {
+  app.delete('/mcp', bearerAuth, mcpRateLimiter, async (req: Request, res: Response) => {
     const sessionId = req.headers['mcp-session-id'] as string | undefined;
 
     if (!sessionId || !transports[sessionId]) {
EOF
@@ -24,6 +24,7 @@
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import { randomUUID } from 'crypto';
import rateLimit from 'express-rate-limit';

// Extend Express Request type to include auth info from SDK middleware
declare module 'express' {
@@ -305,7 +306,15 @@
);

// Handle POST requests to /mcp with bearer auth and scope checking
app.post('/mcp', bearerAuth, adminScopeCheck, async (req: Request, res: Response) => {
const mcpRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each client to 100 requests per windowMs
standardHeaders: true,
legacyHeaders: false,
message: 'Too many requests to the MCP endpoint, please try again later.'
});

app.post('/mcp', bearerAuth, mcpRateLimiter, adminScopeCheck, async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;

try {
@@ -371,7 +380,7 @@
});

// Handle GET requests - SSE streams for sessions (also requires auth)
app.get('/mcp', bearerAuth, async (req: Request, res: Response) => {
app.get('/mcp', bearerAuth, mcpRateLimiter, async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;

if (!sessionId || !transports[sessionId]) {
@@ -393,7 +402,7 @@
});

// Handle DELETE requests - session termination (also requires auth)
app.delete('/mcp', bearerAuth, async (req: Request, res: Response) => {
app.delete('/mcp', bearerAuth, mcpRateLimiter, async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;

if (!sessionId || !transports[sessionId]) {
package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -73,5 +73,8 @@
     },
     "resolutions": {
         "strip-ansi": "6.0.1"
-    }
+    },
+    "dependencies": {
+        "express-rate-limit": "^8.2.1"
 }
+}
EOF
@@ -73,5 +73,8 @@
},
"resolutions": {
"strip-ansi": "6.0.1"
}
},
"dependencies": {
"express-rate-limit": "^8.2.1"
}
}
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 8.2.1 None
Copilot is powered by AI and may make mistakes. Always verify output.
});

// Handle DELETE requests - session termination (also requires auth)
app.delete('/mcp', bearerAuth, async (req: Request, res: Response) => {

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI about 23 hours ago

In general, the fix is to add a rate-limiting middleware (e.g., using express-rate-limit) and apply it to routes that perform authentication/authorization and other potentially expensive operations. This middleware should be configured with a reasonable time window and maximum number of requests per client (typically per IP) and then inserted into the middleware chain before the existing bearerAuth and handler functions.

For this file, the best low-impact fix is:

  1. Import express-rate-limit at the top of src/conformance/auth-test-server.ts.
  2. Define a limiter instance (e.g., const mcpRateLimiter = rateLimit({...})) near where the Express app is configured.
  3. Apply the limiter to the /mcp routes that perform authorization: POST /mcp, GET /mcp, and DELETE /mcp. The existing functionality is preserved; we only add an extra middleware parameter before bearerAuth so that if the request exceeds the limit, it is rejected early.

Concretely:

  • Add import rateLimit from 'express-rate-limit'; alongside other imports.
  • After the Express app is created (wherever that is defined in the same file) or near the route definitions, define a limiter, for example:
const mcpRateLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 60,
  standardHeaders: true,
  legacyHeaders: false
});
  • Update the three route handler registrations:
app.post('/mcp', mcpRateLimiter, bearerAuth, adminScopeCheck, async (req, res) => { ... });

app.get('/mcp', mcpRateLimiter, bearerAuth, async (req, res) => { ... });

app.delete('/mcp', mcpRateLimiter, bearerAuth, async (req, res) => { ... });

This directly addresses all alert variants, as they all concern the /mcp handler using bearerAuth without rate limiting.

Suggested changeset 2
src/conformance/auth-test-server.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/conformance/auth-test-server.ts b/src/conformance/auth-test-server.ts
--- a/src/conformance/auth-test-server.ts
+++ b/src/conformance/auth-test-server.ts
@@ -24,6 +24,7 @@
 import express, { Request, Response, NextFunction } from 'express';
 import cors from 'cors';
 import { randomUUID } from 'crypto';
+import rateLimit from 'express-rate-limit';
 
 // Extend Express Request type to include auth info from SDK middleware
 declare module 'express' {
@@ -305,7 +306,14 @@
   );
 
   // Handle POST requests to /mcp with bearer auth and scope checking
-  app.post('/mcp', bearerAuth, adminScopeCheck, async (req: Request, res: Response) => {
+  const mcpRateLimiter = rateLimit({
+    windowMs: 60 * 1000, // 1 minute
+    max: 60, // limit each IP to 60 requests per window
+    standardHeaders: true,
+    legacyHeaders: false,
+  });
+
+  app.post('/mcp', mcpRateLimiter, bearerAuth, adminScopeCheck, async (req: Request, res: Response) => {
     const sessionId = req.headers['mcp-session-id'] as string | undefined;
 
     try {
@@ -371,7 +379,7 @@
   });
 
   // Handle GET requests - SSE streams for sessions (also requires auth)
-  app.get('/mcp', bearerAuth, async (req: Request, res: Response) => {
+  app.get('/mcp', mcpRateLimiter, bearerAuth, async (req: Request, res: Response) => {
     const sessionId = req.headers['mcp-session-id'] as string | undefined;
 
     if (!sessionId || !transports[sessionId]) {
@@ -393,7 +401,7 @@
   });
 
   // Handle DELETE requests - session termination (also requires auth)
-  app.delete('/mcp', bearerAuth, async (req: Request, res: Response) => {
+  app.delete('/mcp', mcpRateLimiter, bearerAuth, async (req: Request, res: Response) => {
     const sessionId = req.headers['mcp-session-id'] as string | undefined;
 
     if (!sessionId || !transports[sessionId]) {
EOF
@@ -24,6 +24,7 @@
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import { randomUUID } from 'crypto';
import rateLimit from 'express-rate-limit';

// Extend Express Request type to include auth info from SDK middleware
declare module 'express' {
@@ -305,7 +306,14 @@
);

// Handle POST requests to /mcp with bearer auth and scope checking
app.post('/mcp', bearerAuth, adminScopeCheck, async (req: Request, res: Response) => {
const mcpRateLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 60, // limit each IP to 60 requests per window
standardHeaders: true,
legacyHeaders: false,
});

app.post('/mcp', mcpRateLimiter, bearerAuth, adminScopeCheck, async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;

try {
@@ -371,7 +379,7 @@
});

// Handle GET requests - SSE streams for sessions (also requires auth)
app.get('/mcp', bearerAuth, async (req: Request, res: Response) => {
app.get('/mcp', mcpRateLimiter, bearerAuth, async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;

if (!sessionId || !transports[sessionId]) {
@@ -393,7 +401,7 @@
});

// Handle DELETE requests - session termination (also requires auth)
app.delete('/mcp', bearerAuth, async (req: Request, res: Response) => {
app.delete('/mcp', mcpRateLimiter, bearerAuth, async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;

if (!sessionId || !transports[sessionId]) {
package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -73,5 +73,8 @@
     },
     "resolutions": {
         "strip-ansi": "6.0.1"
-    }
+    },
+    "dependencies": {
+        "express-rate-limit": "^8.2.1"
 }
+}
EOF
@@ -73,5 +73,8 @@
},
"resolutions": {
"strip-ansi": "6.0.1"
}
},
"dependencies": {
"express-rate-limit": "^8.2.1"
}
}
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 8.2.1 None
Copilot is powered by AI and may make mistakes. Always verify output.
// Configure CORS to expose Mcp-Session-Id header for browser-based clients
app.use(
cors({
origin: '*',

Check warning

Code scanning / CodeQL

Permissive CORS configuration Medium

CORS Origin allows broad access due to
permissive or user controlled value
.
- Add admin-action tool requiring 'admin' scope
- Add scope-checking middleware for privileged tools
- Returns 403 insufficient_scope for missing admin scope
- Add scopes_supported to PRM response
);

// Handle POST requests to /mcp with bearer auth and scope checking
app.post('/mcp', bearerAuth, adminScopeCheck, async (req: Request, res: Response) => {

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.
This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI about 23 hours ago

In general, to fix missing rate limiting on an Express route that performs authentication/authorization and potentially expensive work, you add a rate-limiting middleware. A common approach is to use a well-known library like express-rate-limit, configure a sensible window and max requests, and apply it either globally or specifically to the sensitive route(s). This ensures that even if an attacker can hit the endpoint, they can’t do so at an unbounded rate.

For this specific /mcp POST handler, the best minimal fix is:

  • Import express-rate-limit.
  • Define a rate limiter specifically for the /mcp endpoint (so we don’t accidentally affect other routes like the metadata endpoint).
  • Insert the limiter middleware into the POST route’s middleware chain, alongside bearerAuth and adminScopeCheck, without changing their behavior.

Concretely, in src/conformance/auth-test-server.ts:

  1. Add a new import near the existing imports: import rateLimit from 'express-rate-limit';.

  2. After const app = express(); and before the route definitions, define a limiter, for example:

    const mcpRateLimiter = rateLimit({
      windowMs: 60 * 1000, // 1 minute
      max: 60, // limit each IP to 60 requests per window
      standardHeaders: true,
      legacyHeaders: false
    });

    This is conservative but still reasonable for a test server; adjust values if the project has a standard.

  3. Update the /mcp route definition to include mcpRateLimiter:

    app.post('/mcp', mcpRateLimiter, bearerAuth, adminScopeCheck, async (req: Request, res: Response) => {

This leaves all auth and request handling logic intact while ensuring the route is rate-limited as required.

Suggested changeset 2
src/conformance/auth-test-server.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/conformance/auth-test-server.ts b/src/conformance/auth-test-server.ts
--- a/src/conformance/auth-test-server.ts
+++ b/src/conformance/auth-test-server.ts
@@ -24,6 +24,7 @@
 import express, { Request, Response, NextFunction } from 'express';
 import cors from 'cors';
 import { randomUUID } from 'crypto';
+import rateLimit from 'express-rate-limit';
 
 // Extend Express Request type to include auth info from SDK middleware
 declare module 'express' {
@@ -277,6 +278,13 @@
   const app = express();
   app.use(express.json());
 
+  const mcpRateLimiter = rateLimit({
+    windowMs: 60 * 1000, // 1 minute
+    max: 60, // limit each IP to 60 requests per minute
+    standardHeaders: true,
+    legacyHeaders: false
+  });
+
   // Configure CORS to expose Mcp-Session-Id header for browser-based clients
   app.use(
     cors({
@@ -304,8 +312,8 @@
     }
   );
 
-  // Handle POST requests to /mcp with bearer auth and scope checking
-  app.post('/mcp', bearerAuth, adminScopeCheck, async (req: Request, res: Response) => {
+  // Handle POST requests to /mcp with bearer auth, scope checking, and rate limiting
+  app.post('/mcp', mcpRateLimiter, bearerAuth, adminScopeCheck, async (req: Request, res: Response) => {
     const sessionId = req.headers['mcp-session-id'] as string | undefined;
 
     try {
EOF
@@ -24,6 +24,7 @@
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import { randomUUID } from 'crypto';
import rateLimit from 'express-rate-limit';

// Extend Express Request type to include auth info from SDK middleware
declare module 'express' {
@@ -277,6 +278,13 @@
const app = express();
app.use(express.json());

const mcpRateLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 60, // limit each IP to 60 requests per minute
standardHeaders: true,
legacyHeaders: false
});

// Configure CORS to expose Mcp-Session-Id header for browser-based clients
app.use(
cors({
@@ -304,8 +312,8 @@
}
);

// Handle POST requests to /mcp with bearer auth and scope checking
app.post('/mcp', bearerAuth, adminScopeCheck, async (req: Request, res: Response) => {
// Handle POST requests to /mcp with bearer auth, scope checking, and rate limiting
app.post('/mcp', mcpRateLimiter, bearerAuth, adminScopeCheck, async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;

try {
package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -73,5 +73,8 @@
     },
     "resolutions": {
         "strip-ansi": "6.0.1"
-    }
+    },
+    "dependencies": {
+        "express-rate-limit": "^8.2.1"
 }
+}
EOF
@@ -73,5 +73,8 @@
},
"resolutions": {
"strip-ansi": "6.0.1"
}
},
"dependencies": {
"express-rate-limit": "^8.2.1"
}
}
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 8.2.1 None
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants